package com.rizzo.android.content.http.util;

import com.google.common.collect.Lists;
import org.apache.http.*;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * Retry pattern can be calculated with:
 * max(MAX_RETRY_DELAY, RETRY_DELAY * (RETRY_BACKOFF_MULTIPLIER)^(executionCount))
 * For scenario RETRY_DELAY = 1000, MAX_RETRY_DELAY = 60000, RETRY_BACKOFF_MULTIPLIER = 2, MAX_RETRIES = 7
 * First retry after -> 	1000 * 2^(1) = 2000ms
 * Second retry after -> 	1000 * 2^(2) = 4000ms
 * Third retry after -> 	1000 * 2^(3) = 8000ms
 * Fourth retry after -> 	1000 * 2^(4) = 16000ms
 * Fifth retry after -> 	1000 * 2^(5) = 32000ms
 * Sixth retry after -> 	1000 * 2^(6) = 60000ms
 * Sevent retry after -> 	1000 * 2^(7) = 60000ms
 */
public class HttpAccessUtil {

    private static final int RETRY_DELAY = 1000;
    private static final int MAX_RETRY_DELAY = 60000;
    private static final int RETRY_BACKOFF_MULTIPLIER = 2;
    private static final int MAX_RETRIES = 10;
    private static final String ANALYTICS_SCHEME = "http";
    private static final String USER_AGENT = "Rizzo-Android/0.1.0";
    private static final String NONCE_CHALLENGE_HEADER = "WWW-Authenticate";
    private static final String APPLICATION_HEADER = "application";
    private static final String ACCEPT_HEADER = "Accept";
    private static final String QUERY_PARAMETERS_ENCODING = "UTF-8";
    private static final int MAX_TOTAL_HTTP_CONNECTIONS = 100;
    private static final int HTTP_CONNECTION_TIMEOUT = 15 * 1000;
    private static final int PORT = 80;

    private static final AbstractHttpClient httpClient;

    static {
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme(ANALYTICS_SCHEME, PlainSocketFactory.getSocketFactory(), PORT));

        HttpParams connManagerParams = new BasicHttpParams();
        ConnManagerParams.setMaxTotalConnections(connManagerParams, MAX_TOTAL_HTTP_CONNECTIONS);
        ConnManagerParams.setMaxConnectionsPerRoute(connManagerParams, new ConnPerRouteBean(MAX_TOTAL_HTTP_CONNECTIONS));
        ConnManagerParams.setTimeout(connManagerParams, HTTP_CONNECTION_TIMEOUT);

        ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(connManagerParams, schemeRegistry);

        HttpParams clientParams = new BasicHttpParams();
        HttpProtocolParams.setUserAgent(clientParams, USER_AGENT);
        HttpConnectionParams.setConnectionTimeout(clientParams, HTTP_CONNECTION_TIMEOUT);
        HttpConnectionParams.setSoTimeout(clientParams, HTTP_CONNECTION_TIMEOUT);
        httpClient = new DefaultHttpClient(cm, clientParams);

        HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(MAX_RETRIES, false) {

            public boolean retryRequest(IOException exception, int executionCount,
                                        HttpContext context) {
                if (!super.retryRequest(exception, executionCount, context)) {
                    return false;
                }
                try {
                    long sleepPeriod = Math.round(Math.max(MAX_RETRY_DELAY, (RETRY_DELAY * Math.pow(RETRY_BACKOFF_MULTIPLIER, executionCount))));
                    Thread.sleep(sleepPeriod);
                } catch (InterruptedException ignored) {
                }
                return true;
            }
        };

        httpClient.setHttpRequestRetryHandler(retryHandler);
    }

    public static String get(String host, String context, String username, String password, String accept, String application,
                             Map<String, String> queryParameters) {
        try {
            HttpHost targetHost = new HttpHost(host);
            final List<NameValuePair> queryNameValuePairs = convertQueryMap(queryParameters);
            HttpGet requestChallenge = getAuthenticatedHttpGet(targetHost, context, username, password, application, queryNameValuePairs);
            if (requestChallenge != null) {
                requestChallenge.setHeader(ACCEPT_HEADER, accept);
                if (application != null) {
                    requestChallenge.setHeader(APPLICATION_HEADER, application);
                }
                HttpResponse responseChallenge = httpClient.execute(targetHost, requestChallenge);
                return EntityUtils.toString(responseChallenge.getEntity());
            } else {
                return "Error: Could not create digest authenticated request due to no nonce in challenge or digest challenge not needed...";
            }
        } catch (IOException e) {
            return "Error: " + e.getMessage();
        } catch (MalformedChallengeException e) {
            return "Error: " + e.getMessage();
        } catch (AuthenticationException e) {
            return "Error: " + e.getMessage();
        }
    }

    private static HttpGet getAuthenticatedHttpGet(HttpHost host, String context, String username, String password, String application,
                                                   List<? extends NameValuePair> queryParameters) throws MalformedChallengeException, IOException, AuthenticationException {
        HttpGet request = new HttpGet(context);
        if (application != null) {
            request.setHeader(APPLICATION_HEADER, application);
        }
        HttpResponse response = httpClient.execute(host, request);
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
            String queryPart = URLEncodedUtils.format(queryParameters, QUERY_PARAMETERS_ENCODING);
            HttpGet authenticatedRequest = new HttpGet(context + "?" + queryPart);
            Header nonceHeader = response.getHeaders(NONCE_CHALLENGE_HEADER)[0];
            if (nonceHeader != null) {
                DigestScheme digestScheme = new DigestScheme();
                digestScheme.processChallenge(nonceHeader);
                authenticatedRequest.addHeader(digestScheme.authenticate(new UsernamePasswordCredentials(username, password), authenticatedRequest));
                return authenticatedRequest;
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    private static List<NameValuePair> convertQueryMap(Map<String, String> queryParameters) {
        final List<NameValuePair> nameValuePairs = Lists.newArrayList();
        if (queryParameters != null) {
            for (Map.Entry<String, String> keyValueEntry : queryParameters.entrySet()) {
                nameValuePairs.add(new BasicNameValuePair(keyValueEntry.getKey(), keyValueEntry.getValue()));
            }
        }
        return nameValuePairs;
    }

}